#include #define PIN_L 5 #define PIN_R 6 #define PIN_START 7 #define PIN_BTN_L 8 #define PIN_BTN_R 9 #define PIN_SPK 10 #define PIN_BRIGHT A4 #define PIN_ZOOM A5 const int LEDS = 50; const uint8_t BRIGHT_MAX = 100; // максимум наместо 255 const unsigned long BRIGHT_UPDATE_MS = 25; const uint8_t BRIGHT_SMOOTH_SHIFT = 3; // 1/8 smoothing (поголемо = помазно) const uint8_t BRIGHT_DEADBAND = 1; // игнорирај +/-1 чекор // ---- LAYOUT ---- const int SCORE_LEDS = 5; // 45..49 const int GAP_LEDS = 5; // 40..44 (always OFF) const int PLAY_LEDS = LEDS - SCORE_LEDS - GAP_LEDS; // 40 (0..39) const int GAP_BOTTOM = PLAY_LEDS; // 40 const int GAP_TOP = PLAY_LEDS + GAP_LEDS - 1; // 44 const int SCORE_BOTTOM = PLAY_LEDS + GAP_LEDS; // 45 const int SCORE_TOP = LEDS - 1; // 49 unsigned long lastBrightUpdate = 0; uint16_t brightFilt = 0; // работи во 0..(BRIGHT_MAX<<8) uint8_t brightApplied = 255; // force first apply // ---- TIMING ---- const unsigned long IDLE_STEP_MS = 30; const unsigned long BLINK_MS = 180; const unsigned long RED_MIN_MS = 4000; const unsigned long RED_MAX_MS = 8000; const unsigned long BAR_STEP_MS = 10; const unsigned long SECOND_PRESS_TIMEOUT_MS = 2000; // Winner baseline scale (fixed) const unsigned long ABS_SCALE_MS = 1000; // 0..1000ms -> 0..40 LEDs // MATCH FLASH (only first 40 LEDs) const byte MATCH_FLASH_TIMES = 3; const unsigned long MATCH_FLASH_PERIOD = 220; // After match flash: show solid winner color for 5 seconds, then go to idle animation const unsigned long FINAL_HOLD_MS = 5000; // Debounce const unsigned long DEBOUNCE_MS = 20; // ---- SOUND ---- const uint16_t IDLE_F_MIN = 100; const uint16_t IDLE_F_MAX = 1200; const uint16_t BLINK_F1 = 300; const uint16_t BLINK_F2 = 500; const uint16_t RED_F = 1000; const unsigned long ROUND_WIN_DELAY_MS = 500; // Round win (first 4 notes of match theme) + pause const uint16_t ROUND_WIN_FREQS[] = { 523, 659, 784, 1046, 0 }; const uint16_t ROUND_WIN_DURS[] = { 180, 180, 200, 260, 120 }; // False-start buzzer (low) const uint16_t FALSE_FREQS[] = { 160, 120, 90, 0, 90, 0 }; const uint16_t FALSE_DURS[] = { 180, 180, 220, 80, 220, 140 }; // Match win sequence (longer) const uint16_t MATCH_FREQS[] = { 523, 659, 784, 1046, 784, 659, 523, 0, 880, 0 }; const uint16_t MATCH_DURS[] = { 120, 120, 140, 200, 120, 120, 180, 90, 260, 150 }; Adafruit_NeoPixel stripL(LEDS, PIN_L, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel stripR(LEDS, PIN_R, NEO_GRB + NEO_KHZ800); uint32_t OFF, RED, GREEN, BLUE; uint32_t L_COL, R_COL; enum State { IDLE, BLINK, REDGO, SHOWBARS_ANIM, RESULT_WAIT_START, MATCHFLASH, MATCH_HOLD, MATCH_IDLE }; State state = IDLE; // ---- debounce state ---- bool stableStart = LOW, stableL = LOW, stableR = LOW; bool lastReadStart = LOW, lastReadL = LOW, lastReadR = LOW; unsigned long lastChangeStart = 0; unsigned long lastChangeL = 0; unsigned long lastChangeR = 0; // ---- idle chase ---- unsigned long lastIdle = 0; int chasePos = 0; byte chaseHue = 0; // ---- blink ---- unsigned long lastBlink = 0; bool blinkToggle = false; unsigned long redGoTime = 0; // ---- round ---- bool lUsed = false, rUsed = false; bool gotL = false, gotR = false; unsigned long tRed = 0; unsigned long reactL = 0, reactR = 0; byte winner = 0; // 1 left, 2 right int targetBarL = 0, targetBarR = 0; int curBarL = 0, curBarR = 0; unsigned long lastBarStep = 0; // ---- score ---- byte scoreL = 0, scoreR = 0; // ---- match flash ---- uint32_t matchWinnerColor = 0; unsigned long lastFlashT = 0; bool flashOn = false; byte flashCount = 0; // ---- final hold timer ---- unsigned long finalHoldStart = 0; // ---- delayed round-win sound trigger ---- bool roundWinSoundPending = false; unsigned long roundWinSoundAt = 0; // ---- LED FREEZE DURING ROUND WIN SOUND ---- bool freezeLeds = false; // when true: DO NOT call show() anywhere int frozenBarL = 0; int frozenBarR = 0; // ================= SOUND PLAYER (non-blocking) ================= struct BeepSeq { const uint16_t* freqs = nullptr; const uint16_t* durs = nullptr; // ms per tone uint8_t len = 0; uint8_t idx = 0; bool active = false; unsigned long nextT = 0; }; BeepSeq seq; int continuousFreq = 0; int currentToneFreq = -1; void soundStop() { noTone(PIN_SPK); currentToneFreq = -1; } void soundSetContinuous(int f) { continuousFreq = f; } void soundStartSeq(const uint16_t* freqs, const uint16_t* durs, uint8_t len) { seq.freqs = freqs; seq.durs = durs; seq.len = len; seq.idx = 0; seq.active = true; seq.nextT = 0; } bool soundSeqActive() { return seq.active; } void soundUpdate(unsigned long now) { if (seq.active) { if (seq.nextT == 0 || (long)(now - seq.nextT) >= 0) { if (seq.idx >= seq.len) { seq.active = false; } else { uint16_t f = seq.freqs[seq.idx]; uint16_t d = seq.durs[seq.idx]; if (f == 0) { noTone(PIN_SPK); currentToneFreq = -1; } else { // fixed duration per note tone(PIN_SPK, f, d); currentToneFreq = (int)f; } seq.nextT = now + d; seq.idx++; } } return; } // No sequence active -> continuous tone if (continuousFreq <= 0) { if (currentToneFreq != -1) soundStop(); } else { if (currentToneFreq != continuousFreq) { tone(PIN_SPK, (unsigned int)continuousFreq); currentToneFreq = continuousFreq; } } } // ================= HELPERS ================= uint32_t wheel(byte p) { p = 255 - p; if (p < 85) return stripL.Color(255 - p * 3, 0, p * 3); if (p < 170) { p -= 85; return stripL.Color(0, p * 3, 255 - p * 3); } p -= 170; return stripL.Color(p * 3, 255 - p * 3, 0); } void showBoth() { if (freezeLeds) return; stripL.show(); stripR.show(); } void clearPlayfield() { for (int i = 0; i < PLAY_LEDS; i++) { stripL.setPixelColor(i, OFF); stripR.setPixelColor(i, OFF); } } void clearGap() { for (int i = GAP_BOTTOM; i <= GAP_TOP; i++) { stripL.setPixelColor(i, OFF); stripR.setPixelColor(i, OFF); } } void setPlayfield(uint32_t c) { for (int i = 0; i < PLAY_LEDS; i++) { stripL.setPixelColor(i, c); stripR.setPixelColor(i, c); } } void drawScore() { clearGap(); for (int i = SCORE_BOTTOM; i <= SCORE_TOP; i++) { stripL.setPixelColor(i, OFF); stripR.setPixelColor(i, OFF); } for (byte i = 0; i < scoreL && i < SCORE_LEDS; i++) stripL.setPixelColor(SCORE_TOP - i, L_COL); for (byte i = 0; i < scoreR && i < SCORE_LEDS; i++) stripR.setPixelColor(SCORE_TOP - i, R_COL); } unsigned long readZoom() { int v = analogRead(PIN_ZOOM); return 10UL + (unsigned long)((v * 990UL) / 1023UL); } int mapAbs(unsigned long ms) { if (ms > ABS_SCALE_MS) ms = ABS_SCALE_MS; unsigned long bar = (ms * (unsigned long)PLAY_LEDS + (ABS_SCALE_MS / 2)) / ABS_SCALE_MS; if (bar > (unsigned long)PLAY_LEDS) bar = PLAY_LEDS; return (int)bar; } int mapDiff(unsigned long diff, unsigned long zoom, int maxExtra) { if (zoom < 10) zoom = 10; if (diff > zoom) diff = zoom; unsigned long extra = (diff * (unsigned long)maxExtra + (zoom / 2)) / zoom; if ((int)extra > maxExtra) extra = maxExtra; return (int)extra; } void resetRoundVars() { lUsed = rUsed = false; gotL = gotR = false; reactL = reactR = 0; winner = 0; targetBarL = targetBarR = 0; curBarL = curBarR = 0; roundWinSoundPending = false; freezeLeds = false; } void startRoundKeepScore(unsigned long now) { state = BLINK; lastBlink = now; blinkToggle = false; redGoTime = now + random(RED_MIN_MS, RED_MAX_MS + 1); resetRoundVars(); clearPlayfield(); drawScore(); showBoth(); soundSetContinuous(BLINK_F1); } void startMatchFlash(uint32_t winColor) { matchWinnerColor = winColor; state = MATCHFLASH; lastFlashT = millis(); flashOn = false; flashCount = 0; soundStartSeq(MATCH_FREQS, MATCH_DURS, sizeof(MATCH_FREQS) / sizeof(MATCH_FREQS[0])); soundSetContinuous(0); } // Draw bars once (used when freezing) void drawBarsStatic(int barL, int barR) { clearPlayfield(); for (int i = 0; i < barL && i < PLAY_LEDS; i++) stripL.setPixelColor(i, L_COL); for (int i = 0; i < barR && i < PLAY_LEDS; i++) stripR.setPixelColor(i, R_COL); drawScore(); showBoth(); } // ---------- setup ---------- void setup() { pinMode(PIN_START, INPUT); pinMode(PIN_BTN_L, INPUT); pinMode(PIN_BTN_R, INPUT); pinMode(PIN_SPK, OUTPUT); pinMode(PIN_BRIGHT, INPUT); pinMode(PIN_ZOOM, INPUT); stripL.begin(); stripR.begin(); OFF = stripL.Color(0, 0, 0); RED = stripL.Color(255, 0, 0); GREEN = stripL.Color(0, 255, 0); BLUE = stripL.Color(0, 0, 255); L_COL = stripL.Color(255, 180, 0); R_COL = stripL.Color(255, 0, 180); randomSeed(analogRead(A0)); state = IDLE; clearPlayfield(); drawScore(); showBoth(); soundSetContinuous(IDLE_F_MIN); } // ---------- loop ---------- void loop() { unsigned long now = millis(); // Brightness // Brightness (filtered + capped + rate limited) if (now - lastBrightUpdate >= BRIGHT_UPDATE_MS) { lastBrightUpdate = now; // map pot -> 0..BRIGHT_MAX uint16_t raw = analogRead(PIN_BRIGHT); // 0..1023 uint16_t target = (raw * BRIGHT_MAX + 511) / 1023; // 0..BRIGHT_MAX // IIR smoothing in fixed point (<<8) uint16_t targetFP = (uint16_t)(target << 8); brightFilt = brightFilt + ((int32_t)targetFP - (int32_t)brightFilt) / (1 << BRIGHT_SMOOTH_SHIFT); uint8_t b = (uint8_t)(brightFilt >> 8); // deadband to avoid flicker from tiny pot noise if (b > brightApplied + BRIGHT_DEADBAND || b + BRIGHT_DEADBAND < brightApplied) { brightApplied = b; stripL.setBrightness(brightApplied); stripR.setBrightness(brightApplied); } } // ---- DEBOUNCE -> edges ---- bool startEdge = false, lEdge = false, rEdge = false; bool readStart = digitalRead(PIN_START); if (readStart != lastReadStart) { lastChangeStart = now; lastReadStart = readStart; } if (now - lastChangeStart > DEBOUNCE_MS) { if (stableStart != readStart) { stableStart = readStart; if (stableStart == HIGH) startEdge = true; } } bool readL = digitalRead(PIN_BTN_L); if (readL != lastReadL) { lastChangeL = now; lastReadL = readL; } if (now - lastChangeL > DEBOUNCE_MS) { if (stableL != readL) { stableL = readL; if (stableL == HIGH) lEdge = true; } } bool readR = digitalRead(PIN_BTN_R); if (readR != lastReadR) { lastChangeR = now; lastReadR = readR; } if (now - lastChangeR > DEBOUNCE_MS) { if (stableR != readR) { stableR = readR; if (stableR == HIGH) rEdge = true; } } // Update sound engine soundUpdate(now); // When round-win sound is active, freeze LED updates completely for clean tone if (freezeLeds) { if (!soundSeqActive()) { // sequence ended -> unfreeze freezeLeds = false; // redraw once (so zoom changes etc. can resume) drawBarsStatic(curBarL, curBarR); } } // Trigger delayed round-win sound ONLY when time comes and nothing else is playing if (roundWinSoundPending && (long)(now - roundWinSoundAt) >= 0 && !soundSeqActive()) { // Freeze LEDs for the whole round-win sequence freezeLeds = true; // show the bars ONCE, then stop calling show() drawBarsStatic(curBarL, curBarR); soundStartSeq(ROUND_WIN_FREQS, ROUND_WIN_DURS, sizeof(ROUND_WIN_FREQS) / sizeof(ROUND_WIN_FREQS[0])); soundSetContinuous(0); roundWinSoundPending = false; } // ---- START behavior ---- if (startEdge) { if (state == MATCH_HOLD || state == MATCH_IDLE) { scoreL = 0; scoreR = 0; startRoundKeepScore(now); return; } if (state != MATCHFLASH) { startRoundKeepScore(now); return; } } // ---- IDLE / MATCH_IDLE ---- if (state == IDLE || state == MATCH_IDLE) { if (now - lastIdle >= IDLE_STEP_MS) { lastIdle = now; clearPlayfield(); uint32_t c = wheel(chaseHue); stripL.setPixelColor(chasePos, c); stripR.setPixelColor(chasePos, c); drawScore(); showBoth(); uint16_t f = (uint16_t)(IDLE_F_MIN + (uint32_t)(IDLE_F_MAX - IDLE_F_MIN) * (uint32_t)chasePos / (uint32_t)(PLAY_LEDS - 1)); soundSetContinuous(f); chasePos++; if (chasePos >= PLAY_LEDS) chasePos = 0; chaseHue += 3; } return; } // ---- BLINK ---- if (state == BLINK) { // false start => lose + low buzzer if (lEdge && !lUsed) { lUsed = true; winner = 2; gotL = gotR = true; reactL = reactR = 0; soundStartSeq(FALSE_FREQS, FALSE_DURS, sizeof(FALSE_FREQS) / sizeof(FALSE_FREQS[0])); soundSetContinuous(0); if (scoreR < SCORE_LEDS) scoreR++; if (scoreR >= 5) { startMatchFlash(R_COL); return; } unsigned long zoom = readZoom(); int base = mapAbs(0); int extra = mapDiff(0, zoom, PLAY_LEDS - base); targetBarR = base; targetBarL = base + extra; curBarL = curBarR = 0; lastBarStep = now; state = SHOWBARS_ANIM; return; } if (rEdge && !rUsed) { rUsed = true; winner = 1; gotL = gotR = true; reactL = reactR = 0; soundStartSeq(FALSE_FREQS, FALSE_DURS, sizeof(FALSE_FREQS) / sizeof(FALSE_FREQS[0])); soundSetContinuous(0); if (scoreL < SCORE_LEDS) scoreL++; if (scoreL >= 5) { startMatchFlash(L_COL); return; } unsigned long zoom = readZoom(); int base = mapAbs(0); int extra = mapDiff(0, zoom, PLAY_LEDS - base); targetBarL = base; targetBarR = base + extra; curBarL = curBarR = 0; lastBarStep = now; state = SHOWBARS_ANIM; return; } if (now - lastBlink >= BLINK_MS) { lastBlink = now; blinkToggle = !blinkToggle; clearPlayfield(); uint32_t c = blinkToggle ? GREEN : BLUE; for (int i = 0; i < 3; i++) { stripL.setPixelColor(i, c); stripR.setPixelColor(i, c); } drawScore(); showBoth(); soundSetContinuous(blinkToggle ? BLINK_F1 : BLINK_F2); } if ((long)(now - redGoTime) >= 0) { tRed = now; clearPlayfield(); for (int i = 0; i < 3; i++) { stripL.setPixelColor(i, RED); stripR.setPixelColor(i, RED); } drawScore(); showBoth(); state = REDGO; lUsed = rUsed = false; gotL = gotR = false; soundSetContinuous(RED_F); return; } return; } // ---- REDGO ---- if (state == REDGO) { if (lEdge && !lUsed) { lUsed = true; gotL = true; reactL = now - tRed; if (winner == 0) winner = 1; } if (rEdge && !rUsed) { rUsed = true; gotR = true; reactR = now - tRed; if (winner == 0) winner = 2; } bool timeout = false; if (winner != 0 && (now - tRed >= SECOND_PRESS_TIMEOUT_MS)) timeout = true; if ((gotL && gotR) || timeout) { if (!gotL) reactL = SECOND_PRESS_TIMEOUT_MS; if (!gotR) reactR = SECOND_PRESS_TIMEOUT_MS; if (winner == 1 && scoreL < SCORE_LEDS) scoreL++; if (winner == 2 && scoreR < SCORE_LEDS) scoreR++; soundSetContinuous(0); if (scoreL >= 5) { startMatchFlash(L_COL); return; } if (scoreR >= 5) { startMatchFlash(R_COL); return; } unsigned long zoom = readZoom(); unsigned long wMs = (winner == 1) ? reactL : reactR; unsigned long lMs = (winner == 1) ? reactR : reactL; int base = mapAbs(wMs); int maxExtra = PLAY_LEDS - base; if (maxExtra < 0) maxExtra = 0; int extra = mapDiff((lMs > wMs) ? (lMs - wMs) : 0, zoom, maxExtra); if (winner == 1) { targetBarL = base; targetBarR = base + extra; } else { targetBarR = base; targetBarL = base + extra; } curBarL = curBarR = 0; lastBarStep = now; state = SHOWBARS_ANIM; } return; } // ---- SHOWBARS_ANIM ---- if (state == SHOWBARS_ANIM) { if (winner != 0) { unsigned long zoom = readZoom(); unsigned long wMs = (winner == 1) ? reactL : reactR; unsigned long lMs = (winner == 1) ? reactR : reactL; int base = mapAbs(wMs); int maxExtra = PLAY_LEDS - base; if (maxExtra < 0) maxExtra = 0; int extra = mapDiff((lMs > wMs) ? (lMs - wMs) : 0, zoom, maxExtra); if (winner == 1) { targetBarL = base; targetBarR = base + extra; } else { targetBarR = base; targetBarL = base + extra; } } if (now - lastBarStep >= BAR_STEP_MS) { lastBarStep = now; if (curBarL < targetBarL) curBarL++; if (curBarR < targetBarR) curBarR++; clearPlayfield(); for (int i = 0; i < curBarL && i < PLAY_LEDS; i++) stripL.setPixelColor(i, L_COL); for (int i = 0; i < curBarR && i < PLAY_LEDS; i++) stripR.setPixelColor(i, R_COL); drawScore(); showBoth(); if (curBarL >= targetBarL && curBarR >= targetBarR) { state = RESULT_WAIT_START; // schedule clean round win sound after bars shown roundWinSoundPending = true; roundWinSoundAt = now + ROUND_WIN_DELAY_MS; } } return; } // ---- RESULT_WAIT_START ---- if (state == RESULT_WAIT_START) { // If LEDs are frozen due to sound -> do not update visuals if (freezeLeds) return; unsigned long zoom = readZoom(); unsigned long wMs = (winner == 1) ? reactL : reactR; unsigned long lMs = (winner == 1) ? reactR : reactL; int base = mapAbs(wMs); int maxExtra = PLAY_LEDS - base; if (maxExtra < 0) maxExtra = 0; int extra = mapDiff((lMs > wMs) ? (lMs - wMs) : 0, zoom, maxExtra); if (winner == 1) { targetBarL = base; targetBarR = base + extra; } else { targetBarR = base; targetBarL = base + extra; } static unsigned long lastT = 0; if (now - lastT >= BAR_STEP_MS) { lastT = now; if (curBarL < targetBarL) curBarL++; else if (curBarL > targetBarL) curBarL--; if (curBarR < targetBarR) curBarR++; else if (curBarR > targetBarR) curBarR--; clearPlayfield(); for (int i = 0; i < curBarL && i < PLAY_LEDS; i++) stripL.setPixelColor(i, L_COL); for (int i = 0; i < curBarR && i < PLAY_LEDS; i++) stripR.setPixelColor(i, R_COL); drawScore(); showBoth(); } return; } // ---- MATCHFLASH ---- if (state == MATCHFLASH) { if (now - lastFlashT >= MATCH_FLASH_PERIOD) { lastFlashT = now; flashOn = !flashOn; if (flashOn) setPlayfield(matchWinnerColor); else clearPlayfield(); drawScore(); showBoth(); if (!flashOn) { flashCount++; if (flashCount >= MATCH_FLASH_TIMES) { setPlayfield(matchWinnerColor); drawScore(); showBoth(); finalHoldStart = now; state = MATCH_HOLD; } } } return; } // ---- MATCH_HOLD ---- if (state == MATCH_HOLD) { if (now - finalHoldStart >= FINAL_HOLD_MS) { state = MATCH_IDLE; lastIdle = now; chasePos = 0; chaseHue = 0; clearPlayfield(); drawScore(); showBoth(); soundSetContinuous(IDLE_F_MIN); } return; } }